/******************************************************************************
	This file is a part of the Snapshot Bioinformatics Project

	Copyright (C) 2012-2013 Jarom Schow

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/

#include "GzFile.h"

#include <QtCore/QFile>

#include "zlib.h"

using namespace Bio;

#define GZ_WINDOW_BITS	31

typedef Bytef ZlibByte;
typedef uInt ZlibSize;

struct GzFile::Data {
	enum State {
		// Read state
		NotReadFirstByte,
		InStream,
		EndOfStream,
		// Write state
		NoBytesWritten,
		BytesWritten,
		// Common
		Closed,
		Error
	};

	Data() : m_bufferSize( 65536 ), m_compressionLevel( Z_DEFAULT_COMPRESSION ), m_buffer( NULL ), m_state( Closed ), 
		m_device( NULL ), m_manageDevice( false ), m_ownDevice( false )
	{
		m_stream.zalloc = Z_NULL;
		m_stream.zfree = Z_NULL;
		m_stream.opaque = Z_NULL;
	}

	int			m_bufferSize;
	int			m_compressionLevel;
	char		*m_buffer;
	bool		m_manageDevice, m_ownDevice;
	z_stream	m_stream;
	State		m_state;
	QIODevice	*m_device;
};

/**
**/
GzFile::GzFile( const QString &filename, int compressionLevel, int bufferSize ) : m_data( new Data )
{
	m_data->m_bufferSize = bufferSize;
	if( m_data->m_bufferSize < 0 )
		m_data->m_bufferSize = 65536;

	if( compressionLevel < 0 || compressionLevel > 9 )
		compressionLevel = Z_DEFAULT_COMPRESSION;

	m_data->m_compressionLevel = compressionLevel;

	m_data->m_buffer = new char[m_data->m_bufferSize];
	m_data->m_device = new QFile( filename );
	m_data->m_ownDevice = true;
}

/**
**/
GzFile::GzFile( QIODevice *device, int compressionLevel, int bufferSize ) : m_data( new Data )
{
	m_data->m_bufferSize = bufferSize;
	if( m_data->m_bufferSize < 0 )
		m_data->m_bufferSize = 65536;

	if( compressionLevel < 0 || compressionLevel > 9 )
		compressionLevel = Z_DEFAULT_COMPRESSION;

	m_data->m_compressionLevel = compressionLevel;

	m_data->m_buffer = new char[m_data->m_bufferSize];
	m_data->m_device = device;
}

/**
**/
GzFile::~GzFile()
{
	close();

	delete [] m_data->m_buffer;
	if( m_data->m_ownDevice )
		delete m_data->m_device;
	delete m_data;
	m_data = NULL;
}

/**
**/
bool GzFile::isSequential() const
{
	return true;
}

/**
**/
bool GzFile::open( OpenMode mode )
{
	if( isOpen() ) {
		qWarning( "File already open in call to GzFile::open()" );
		return false;
	}

	bool	read = (mode & ReadOnly) != 0;
	bool	write = (mode & WriteOnly) != 0;
	int		status;

	if( (read && write) || (!read && !write) ){
		qWarning( "Invalid mode passed to GzFile::open. Only ReadOnly and WriteOnly are supported." );
		return false;
	}

	if( m_data->m_device->isOpen() ) {
		OpenMode	deviceMode = m_data->m_device->openMode();
		
		m_data->m_manageDevice = false;
		
		if( read && !(deviceMode & ReadOnly) ) {
			qWarning( "DzEncryptIO::open: underlying device must be open in one of the ReadOnly or WriteOnly modes" );
			return false;
		}
		else if (write && !(deviceMode & WriteOnly)) {
			qWarning ("DzEncryptIO::open: underlying device must be open in one of the ReadOnly or WriteOnly modes" );
			return false;
		}
	}
	else {
		m_data->m_manageDevice = true;
		if( !m_data->m_device->open( mode ) ) {
			setErrorString( QString( "Error opening underlying device: " ) + m_data->m_device->errorString() );
			return false;
		}
	}

	// Initialize zlib
	if( read ){
		m_data->m_state = Data::NotReadFirstByte;
		m_data->m_stream.avail_in = 0;
		m_data->m_stream.next_in = 0;
		status = inflateInit2( &m_data->m_stream, GZ_WINDOW_BITS );
	}
	else {
		m_data->m_state = Data::NoBytesWritten;
		status = deflateInit2( &m_data->m_stream, m_data->m_compressionLevel, Z_DEFLATED, GZ_WINDOW_BITS, 8, Z_DEFAULT_STRATEGY );
	}

	// Handle error.
	if( status != Z_OK ) {
		setZlibError( "Internal zlib error: ", status );
		return false;
	}

	return QIODevice::open( mode );
}

/**
**/
void GzFile::close()
{
	if( !isOpen() )
		return;

	emit aboutToClose();

	// Flush and close the zlib stream.
	if( openMode() & ReadOnly ) {
		m_data->m_state = Data::NotReadFirstByte;
		inflateEnd( &m_data->m_stream );
	}
	else {
		if( m_data->m_state == Data::BytesWritten ) { // Only flush if we have written anything.
			m_data->m_state = Data::NoBytesWritten;
			flushZlib( Z_FINISH );
		}
		deflateEnd( &m_data->m_stream );
	}

	if( m_data->m_manageDevice )
		m_data->m_device->close();

	QIODevice::close();
}

/**
**/
void GzFile::flush()
{
	if( !isOpen() || openMode() & ReadOnly )
		return;

	flushZlib( Z_SYNC_FLUSH );
}

/**
**/
const QIODevice* GzFile::getSourceDevice() const
{
	return m_data->m_device;
}

/**
**/
qint64 GzFile::bytesAvailable() const
{
	if( !isOpen() || openMode() & WriteOnly )
		return 0;

	int numBytes = 0;

	switch (m_data->m_state) {
		case Data::NotReadFirstByte:
			numBytes = int( m_data->m_device->bytesAvailable() );
		break;
		case Data::InStream:
			numBytes = 1;
		break;
		case Data::EndOfStream:
		case Data::Error:
		default:
			numBytes = 0;
		break;
	};

	numBytes += m_data->m_device->bytesAvailable();

	if( numBytes > 0 )
		return 1;
	else
		return 0;
}

/**
**/
qint64 GzFile::readData( char *data, qint64 maxSize )
{
	if( m_data->m_state == Data::EndOfStream )
		return 0;

	if( m_data->m_state == Data::Error )
		return -1;

	// We are ging to try to fill the data buffer
	m_data->m_stream.next_out = reinterpret_cast<ZlibByte *>( data );
	m_data->m_stream.avail_out = (unsigned int)( maxSize );

	int status;
	do {
		// Read data if if the input buffer is empty. There could be data in the buffer
		// from a previous readData call.
		if (m_data->m_stream.avail_in == 0) {
			qint64 bytesAvalible = m_data->m_device->read( m_data->m_buffer, m_data->m_bufferSize );
			m_data->m_stream.next_in = reinterpret_cast<Bytef*>( m_data->m_buffer );
			m_data->m_stream.avail_in = (unsigned int)(bytesAvalible);

			if (bytesAvalible == -1) {
				m_data->m_state = Data::Error;
				return -1;
			}

			if (m_data->m_state != Data::InStream) {
				// If we are not in a stream and get 0 bytes, we are probably trying to read from an empty device.
				if(bytesAvalible == 0)
					return 0;
				else if (bytesAvalible > 0)
					m_data->m_state = Data::InStream;
			}
		}

		// Decompress.
		status = inflate(&m_data->m_stream, Z_SYNC_FLUSH);
		switch (status) {
			case Z_NEED_DICT:
			case Z_DATA_ERROR:
			case Z_MEM_ERROR:
				m_data->m_state = Data::Error;
				setZlibError( "Internal zlib error when decompressing: ", status );
				return -1;
			case Z_BUF_ERROR: // No more input and zlib can not privide more output - Not an error, we can try to read again when we have more input.
				return 0;
			break;
		}
	// Loop util data buffer is full or we reach the end of the input stream.
	} while (m_data->m_stream.avail_out != 0 && status != Z_STREAM_END);

	if (status == Z_STREAM_END)
		m_data->m_state = Data::EndOfStream;

	const ZlibSize outputSize = ZlibSize(maxSize - m_data->m_stream.avail_out);
	return outputSize;
}

/**
**/
qint64 GzFile::writeData( const char *data, qint64 maxSize )
{
	if( maxSize < 1 )
		return 0;

	m_data->m_stream.next_in = reinterpret_cast<ZlibByte *>(const_cast<char *>(data));
	m_data->m_stream.avail_in = (unsigned int)(maxSize);

	if( m_data->m_state == Data::Error )
		return -1;

	do {
		m_data->m_stream.next_out = reinterpret_cast<Bytef*>( m_data->m_buffer );
		m_data->m_stream.avail_out = m_data->m_bufferSize;

		int status = deflate( &m_data->m_stream, Z_NO_FLUSH );

		if( status != Z_OK ){
			m_data->m_state = Data::Error;
			setZlibError( "Internal zlib error when compressing: ", status );
			return -1;
		}

		ZlibSize outputSize = m_data->m_bufferSize - m_data->m_stream.avail_out;

		// Try to write data from the buffer to to the underlying device, return -1 on failure.
		if ( writeBytes( m_data->m_buffer, outputSize ) == false )
			return -1;

	} while (m_data->m_stream.avail_out == 0); // run until output is not full.
	Q_ASSERT(m_data->m_stream.avail_in == 0);

	return maxSize;
}

/**
**/
void GzFile::flushZlib( int flushMode )
{
	// No input.
	m_data->m_stream.next_in = 0;
	m_data->m_stream.avail_in = 0;
	int status;
	do {
		m_data->m_stream.next_out = reinterpret_cast<Bytef*>( m_data->m_buffer );
		m_data->m_stream.avail_out = m_data->m_bufferSize;
		status = deflate(&m_data->m_stream, flushMode);
		if (status != Z_OK && status != Z_STREAM_END) {
			m_data->m_state = Data::Error;
			setZlibError( "Internal zlib error when compressing: ", status );
			return;
		}

		ZlibSize outputSize = m_data->m_bufferSize - m_data->m_stream.avail_out;

		// Try to write data from the buffer to to the underlying device, return on failure.
		if (!writeBytes(m_data->m_buffer, outputSize))
			return;

	// If the mode is Z_FNISH we must loop until we get Z_STREAM_END,
	// else we loop as long as zlib is able to fill the output buffer.
	} while ((flushMode == Z_FINISH && status != Z_STREAM_END) || (flushMode != Z_FINISH && m_data->m_stream.avail_out == 0));

	if (flushMode == Z_FINISH)
		Q_ASSERT(status == Z_STREAM_END);
	else
		Q_ASSERT(status == Z_OK);
}

/**
**/
bool GzFile::writeBytes( char *buffer, int outputSize )
{
	ZlibSize totalBytesWritten = 0;
	// Loop until all bytes are written to the underlying device.
	do {
		const qint64 bytesWritten = m_data->m_device->write( m_data->m_buffer, outputSize );
		if (bytesWritten == -1) {
//			device->setErrorString(QT_TRANSLATE_NOOP("QtIOCompressor", "Error writing to underlying device: ") + device->errorString());
			return false;
		}
		totalBytesWritten += bytesWritten;
	} while (totalBytesWritten != outputSize);

	// put up a flag so that the device will be flushed on close.
	m_data->m_state = Data::BytesWritten;
	return true;
}

/**
**/
void GzFile::setZlibError( const QString &errorMessage, int zlibErrorCode )
{
	// Watch out, zlibErrorString may be null.
	const char * const zlibErrorString = zError(zlibErrorCode);
	QString errorString;
	if (zlibErrorString)
		errorString = errorMessage + zlibErrorString;
	else
		errorString = errorMessage + " Unknown error, code " + QString::number(zlibErrorCode);

//	q->setErrorString(errorString);
}